/*
*
*	The Gimme ECMAScript Library by Stephen Stchur
*	"Taking the pain out of Javascript!"
*
*	Copyright (c) Microsoft Corporation.  All rights reserved.
*
*	Description:
*	Gimme Object Module:	Wrapper around Gimme Query
*
*	Requires:
*	Gimme.query.js
*
*/

// Wrap a new Gimme.object(..) invocation in the function g(..)
function g(selector)
{
	return new Gimme.object(Gimme.query(selector));
}

Gimme.object = function(elems)
{
	this.entities = elems;
	this.length = this.entities.length;
};
Gimme.ext = Gimme.object.prototype;

(function()
{
	var helper = Gimme.helper;
	var contains = helper.contains;
	var indexOf = helper.indexOf;
	var normalize = helper.normalize;
	var hasClass = helper.hasClass;
	var readAttr = helper.readAttribute;
	var getSibling = helper.getSibling;

	Gimme.ext.element = function(n)
	{
		return this.entities[n || 0];
	};

	Gimme.ext.parent = function(n)
	{
		return this.entities[n || 0].parentNode;
	};

	Gimme.ext.addClass = function(_class)
	{
		var classes = _class.split(/\s+/);
		
		this.forEach(function(_elem)
		{
			g(classes).forEach(function(_className)
			{
				if (!hasClass(_elem, [ _className ]))
				{
					if (_elem.className === '')
					{
						_elem.className = _className;
					}
					else
					{
						_elem.className += ' ' + _className;
					}
				}
			});
		});
		
		return this;
	};

	Gimme.ext.removeClass = function(_class)
	{
		return this.swapClass(_class, '$1');
	};

	Gimme.ext.swapClass = function(_oldClass, _newClass)
	{
		// if _newClass is '$1' it means we're actually just removing _oldClass (and not replacing it with anything)
		// however, if _newClass is anything other than $1, we need to pad it with spaces 
		if (_newClass !== '$1')	{ _newClass = ' ' + _newClass + ' '; }
		
		var classes = _oldClass.split(/\s+/);
		
		this.forEach(function(_elem)
		{
			var tmpClass = _elem.className;
			
			g(classes).forEach(function(_className)
			{
				var pattern = new RegExp('(^| )' + _className + '( |$)');
				tmpClass = tmpClass.replace(pattern, _newClass);
			});
			
			_elem.className = normalize(tmpClass);
		});
		
		return this;
	};

	Gimme.ext.hasClass = function(_class, n)
	{
		return hasClass(this.entities[n || 0], [ _class ]);
	};

	Gimme.ext.getAncestor = function(_distance, n)
	{
		var elem = this.entities[n || 0];
		var i = _distance;
		while (i-- > 0)
		{
			if (elem)
			{
				elem = elem.parentNode;
			}
			else
			{
				break;
			}			
		}
		
		return elem;
	};

	Gimme.ext.getSibling = function(_distance, n)
	{
		return getSibling(this.entities[n || 0], _distance);
	};

	Gimme.ext.select = function(_selector)
	{
		return new Gimme.object(Gimme.query(_selector, this.entities[0]));
	};

	Gimme.ext.setHTML = function(_html)
	{
		this.forEach(function(_elem)
		{
			_elem.innerHTML = _html;
		});

		return this;
	};

	Gimme.ext.getHTML = function(n)
	{
		return this.entities[n || 0].innerHTML;
	};

	Gimme.ext.setValue = function(_val)
	{
		this.forEach(function(_elem)
		{
			if (typeof _elem.value !== 'undefined')
			{
				_elem.value = _val;
			}
		});
			
		return this;
	};

	Gimme.ext.getValue = function(n)
	{
		return this.entities[n || 0].value || '';
	};

	Gimme.ext.readAttribute = function(_attr, n)
	{
		return readAttr(this.entities[n || 0], _attr);
	};

	Gimme.ext.writeAttribute = function(_attr, _val)
	{
		this.forEach(function(_elem)
		{
			_elem.setAttribute(_attr, _val);
		});
		
		return this;
	};

	Gimme.ext.filter = function(_filterFn)
	{
		var entities = this.entities;
		var i = 0, len = entities.length;
		while (i < len)
		{
			if (!_filterFn(entities[i]))
			{
				entities.splice(i, 1);
				len--;
			}
			else
			{
				i++;
			}
		}
		
		return this;
	};

	Gimme.ext.iterate = function(_callback)
	{
		var entities = this.entities;
		var i, len = entities.length;
		for (i = 0; i < len; i++)
		{
			var gElem = g(entities[i]);
			_callback.call(gElem, i);
		}
		
		_callback = null;
		
		return this;
	};

	Gimme.ext.getStyle = function(_style, n, _smart)
	{
		var elem = this.entities[n || 0];
		
		// TODO:  Document this rather confusing piece of code and why it's necessary
		if (_style === 'opacity')
		{
			var opacity = getOpacity(elem);
			if (isNaN(opacity))
			{
				_style = opacity;
			}
			else
			{
				return opacity;
			}
		}
		
		// _smart, which default to true, means that the function will attempt to calculate
		// the 'bottom' or 'right' styles based on the 'top' or 'left' styles (if it needs to)
		if (_smart !== false) { _smart = true; }

		var computedStyle = '';						// will be set to a computedStyle object in W3C browsers, and a currentStyle object in IE
		
		// if the browser supports .defaultView.getComputedStyle, life is easy -- just use that
		if (typeof document.defaultView !== 'undefined' && typeof document.defaultView.getComputedStyle !== 'undefined')
		{
			computedStyle = document.defaultView.getComputedStyle(elem, null);
		}
		// otherwise, crap, we've got work to do
		else if (typeof elem.currentStyle !== 'undefined')
		{	
			// start by retrieving the element's .currentStyle property (this won't be normalized to pixels)
			computedStyle = elem.currentStyle;
			
			// If the style being requested is either of the positions: top or left, we'll use .offsetTop and .offsetLeft
			// * WARNING:
			// * Browsers have sharp disagreements about what .offsetTop and .offsetLeft mean, but it just so "happens"
			// * that in IE .offsetTop and .offsetLeft are equal to the computed top and the computed left. Firefox usually
			// * agrees with IE on this, but Konqueror, Opera, and Safari all disagree (with IE and FF).
			// * The major flaw in this function's logic is that it assumes .offsetTop and .offsetLeft are going to be the same
			// * as the computed top and computed left if the browser supports .currentStyle (and not .defaultView.getComputedStyle).
			// * At present, this appears to be true, and I suspect that it will always be true, but there is no guarantee.				
			
			// attempt to get the requested style
			var val = computedStyle[_style];
			
			// if the value is 'auto', it most likely means that it was never specified in the stylesheet
			if (val === 'auto')
			{
				// if height was requested, try .clientHeight; if that fails, try .offsetHeight
				if (_style === 'height')
				{
					return (elem.clientHeight || elem.offsetHeight) + 'px';
				}
				
				// if width was requested, try .clientWidth; if that fails, try .offsetWidth
				if (_style === 'width')
				{
					return (elem.clientWidth || elem.offsetWidth) + 'px';
				}
				
				// if top was requested, use .offsetTop
				if (_style === 'top')
				{
					return elem.offsetTop + 'px';
				}
				// if left was requested, use .offsetLeft
				else if (_style === 'left')
				{
					return elem.offsetLeft + 'px';
				}
				// If bottom or right were requested, AND if _smart (true by default) was specified, then
				// try to calculate right based on left or bottom based on top
				else if (_smart && (_style === 'right' || _style === 'bottom'))
				{
					// flip serves as an easy was to request the compliment property
					var flip = { bottom: [ 'top', 'Height' ], right: [ 'left', 'Width' ] };
					
					// get the computedStyle of the compliment property (make sure _smart is false so we don't recurse infinitely)
					var compliment = parseInt(this.getComputedStyle(elem, flip[_style][0], false), 10);
					
					// return the calculated bottom or right based on the known top or left
					return (elem.parentNode['client' + flip[_style][1]] - elem['offset' + flip[_style][1]] - compliment) + 'px';
				}
				// if val is 'auto', but none of the above apply, assume 0px (this might not be safe -- need to look into this more)					
				else
				{
					return '0px';
				}
			}
			
			// Some values should not be converted to pixels (like hex color values for instance), so this regex aids in
			// determining when to try converting to pixels (specifically, only for valid units that are not pixels)
			var nonPixels = /(em|ex|%|in|cm|mm|pt|pc|small|medium|large|thin|thick)$/;
			
			// Based on the 
			var borderWidth = /border(.*)Width/i;
			var borderStyle = borderWidth.test(_style) ? _style.replace(borderWidth, 'border$1Style') : null;				
			
			// only bother attempting to convert valid units that are not already pixels
			if (nonPixels.test(val))
			{					
				// IE does something interesting.  If you ask for border[Top|Right|Bottom|Left]Width, IE will give it to you... NO MATTER WHAT!
				// Depending on who you ask, this may or may not make sense.  But it doesn't matter, it won't work for this function.
				// If, for example, the borderTopWidth is 5px, but the borderTopStyle is 'none', then we need to report borderTopWidth as 0!
				if (borderStyle !== null && computedStyle[borderStyle] === 'none')
					{ return '0px'; }
				else
					{ return convertToPixels(val, elem); }
			}
		}
		return computedStyle[_style];
	};

	Gimme.ext.setStyle = function(_style, _val)
	{
		this.forEach(function(_elem)
		{
			if (_style === 'opacity')
			{
				setOpacity(_elem, _val);
			}
			else
			{
				_elem.style[_style] = _val;
			}
		});
		
		return this;
	};

	Gimme.ext.addEvent = function(_evtName, _fn, _useCapture)
	{
		this.forEach(function(_elem)
		{
			// W3C
			if (typeof _elem.addEventListener !== 'undefined')
			{
				if (_evtName === 'mouseenter')
				{
					_elem.addEventListener('mouseover', mouseEnter(_fn), _useCapture);
				}
				else if (_evtName === 'mouseleave')
				{
					_elem.addEventListener('mouseout', mouseEnter(_fn), _useCapture);
				} 
				else
				{
					_elem.addEventListener(_evtName, _fn, _useCapture);
				}
			}
			// IE
			else if (typeof _elem.attachEvent !== 'undefined')
			{
				// create a key to identify this element/event/function combination
				var key = '{' + ie_getUniqueID(_elem) + '/' + _evtName + '/' + getObjectGUID(_fn) + '}';
				
				// if this element/event/combo has already been wired up, just return
				var f = evtHash[key];
				if (typeof f !== 'undefined')
					{ return; }
				
				// create a helper function to fix IE's lack of standards support
				f = function(e)
				{
					// map .target to .srcElement
					e.target = e.srcElement;
					
					// map .relatedTarget to either .toElement or .fromElement
					if (_evtName == 'mouseover') { e.relatedTarget = e.fromElement; }
					else if (_evtName == 'mouseout') { e.relatedTarget = e.toElement; }
						
					e.preventDefault = function() { e.returnValue = false; };
					e.stopPropagation = function() { e.cancelBubble = true; };
					
					// call the actual function, using entity (the element) as the 'this' object
					_fn.call(_elem, e);
					
					// null out these properties to prevent memory leaks
					e.target = null;
					e.relatedTarget = null;
					e.preventDefault = null;
					e.stopPropagation = null;
					e = null;
				};
				
				// add the helper function to the event hash
				evtHash[key] = f;

				// hook up the event (IE style)
				_elem.attachEvent('on' + _evtName, f);
				
				key = null;
				f = null;
			}
		});
		
		return this;

	};

	Gimme.ext.removeEvent = function(_evtName, _fn, _useCapture)
	{
		this.forEach(function(_elem)
		{
			// W3C
			if (typeof _elem.removeEventListener !== 'undefined')
			{
				if (_evtName === 'mouseenter')
				{
					_elem.removeEventListener('mouseover', mouseEnter(_fn), _useCapture);
				}
				else if (_evtName === 'mouseleave')
				{
					_elem.removeEventListener('mouseout', mouseEnter(_fn), _useCapture);
				}
				else
				{
					_elem.removeEventListener(_evtName, _fn, _useCapture);
				}
			}
			// IE
			else if (typeof _elem.detachEvent !== 'undefined')
			{
				// grab the event handler function from the hash based on its key
				var key = '{' + ie_getUniqueID(_elem) + '/' + _evtName + '/' + getObjectGUID(_fn) + '}';
				var f = evtHash[key];
				if (typeof f !== 'undefined')
				{
					// unhook the event and delete the element from the event hash
					_elem.detachEvent('on' + _evtName, f);
					delete evtHash[key];
				}

				// prevent IE memory leaks
				key = null;
				f = null;
			}
		});
		
		return this;
	};
	
	Gimme.ext.forEach = function()
	{
		return typeof Array.prototype.forEach !== 'undefined' ? native_forEach : custom_forEach;
		
		function native_forEach(_fn, _thisObject)
		{
			this.entities.forEach(_fn, _thisObject);
			return this;
		}
		
		function custom_forEach(_fn, _thisObject)
		{
			var things = this.entities;
			var i, elem, len = things.length;
			for (i = 0; i < len; i++)
			{
				elem = things[i];
				_fn.call(_thisObject, elem, i, things);
			}
			
			return this;
		}
	}();

	Gimme.ext.map = function()
	{
		return typeof Array.prototype.map !== 'undefined' ? native_map : custom_map;
		
		function native_map(_fn, _thisObject)
		{
			return this.entities.map(_fn, _thisObject);
		}
		
		function custom_map(_fn, _thisObject)
		{
			var result = [];
			this.forEach(function(_el, _i)
			{
				result.push(_fn.call(_thisObject, _el));
			});
			
			return result;
		}
	}();

	Gimme.ext.contains = function(_item)
	{
		return contains(this.entities, _item);
	};

	Gimme.ext.indexOf = function(_item)
	{
		return indexOf(this.entities, _item);
	};	

	
	// Helper Area: private members and functions
	//================================
	
	// private members
	var m_guidCounter = 0;
	var evtHash = {};
	
	// private functions
	var setOpacity = function()
	{
		function w3c_opacity(_elem, _amt) { _elem.style.opacity = _amt; }		
		function ie_opacity(_elem, _amt) { _elem.style.filter = 'alpha(opacity=' + (_amt * 100) + ')'; }
	
		var fn, d = document.createElement('div');
		if (typeof d.style.opacity !== 'undefined')
		{
			fn = w3c_opacity;
		}
		else if (typeof d.style.filter !== 'undefined')
		{
			fn = ie_opacity;
		}
		else
		{
			fn = function() {};			// No opacity support I guess :-(
		}
		
		d = null;
		return fn;	
	}();

	var getOpacity = function()
	{
		function w3c_opacity(_elem)	{ return parseFloat(_elem.style.opacity) || 'opacity';	}
		function ie_opacity(_elem)
		{
			var filter = _elem.currentStyle.filter;
			var match = filter.match(/pacity\s*=\s*(\d{1,3}.?\d*)\)/);
			if (!match)
			{
				return 1;
			}
			else
			{
				return parseFloat(match[1]) / 100;
			}
		}
		
		var fn, d = document.createElement('div');
		if (typeof d.style.opacity !== 'undefined')
		{
			fn = w3c_opacity;
		}
		else if (typeof d.style.filter !== 'undefined')
		{
			fn = ie_opacity;
		}
		else
		{
			fn = function() {};			// No opacity support I guess :-(
		}
		
		d = null;
		return fn;	
	}();
	
	function getObjectGUID(_obj)
	{
		if (typeof _obj.guid === 'undefined')
		{
			_obj.guid = m_guidCounter++;
		}
			
		return _obj.guid;	
	}
	
	function ie_getUniqueID(_elem)
	{
		return _elem === window ? 'theWindow' : _elem === document ? 'theDocument' : _elem.uniqueID;	
	}	

	function isAnAncestorOf(_ancestor, _descendant, _generation)
	{
		if (_ancestor === _descendant) { return false; }
		
		var gen = 0;
		while (_descendant && _descendant != _ancestor)
		{
			gen++;
			_descendant = _descendant.parentNode;
		}
		
		_generation = _generation || gen;
		return _descendant === _ancestor && _generation === gen;		
	}
	
	function mouseEnter(_fn)
	{
		var key = getObjectGUID(_fn);
		var f = evtHash[key];
		if (typeof f === 'undefined')
		{
			f = evtHash[key] = function(_evt)
			{
				var relTarget = _evt.relatedTarget;
				if (this === relTarget || isAnAncestorOf(this, relTarget)) { return; }
		
				_fn.call(this, _evt);
			};
		}
		return f;	
	}

	function convertToPixels(_str, _context)
	{
		// if the units are already pixels, no conversion is necessary, just parseInt and return
		if (/px$/.test(_str)) { return parseInt(_str, 10); }
		
		// if no _context was specified, default to the <body>
		if (!_context) { _context = document.body; }
		
		// If we get here, we've got some work to do:
		// 1. Create an invisible div that we'll use to measure the style represented by _str
		// 2. Make sure its position is absolute so it doesn't shift other elements on the page
		// 3. Make sure line-height is 0 or IE will add mysterious extra space to our measurements
		var tmp = document.createElement('div');
		tmp.style.visbility = 'hidden';
		tmp.style.position = 'absolute';				
		tmp.style.lineHeight = '0';				
		
		// Two additional checks are needed before we conver to pixels
		// 1.  If we're tyring to convert a % to pixels, we have to do it in the context of a parent element
		// 2.  Or if the _context element is an <img>, we also need to use the parent element, b/c we're going to append the
		// invisible div (tmp) to the _context, but it's not valid to append an element to an <img> tag
		if (/%$/.test(_str) || _context.tagName === 'IMG')
		{
			_context = _context.parentNode || _context;
			tmp.style.height = _str;
		}
		else
		{
			tmp.style.borderStyle = 'solid';
			tmp.style.borderBottomWidth = '0';					
			tmp.style.borderTopWidth = _str;
		}
		
		_context.appendChild(tmp);
		var px = tmp.offsetHeight;
		_context.removeChild(tmp);
		
		return px || 0;	
	}
	
})();

